# Dynamic Import
首先我们要知道懒加载和动态导入是两个概念
懒加载是一种优化技术,指的是在真正需要某个资源时才加载它
动态导入是一种在代码中按需加载模块的方法,而不是在应用程序启动时一次性加载所有模块,体现在浏览器Network中按需加载模块(js文件)。在 Webpack、Vite 等构建工具中,动态导入会将导入的模块打包成一个单独的文件,只有当代码执行到该导入语句时才会请求该文件。
import './styles.css';
import { createBrowserRouter, RouteObject, RouterProvider } from 'react-router-dom';
import { Suspense, lazy } from 'react';
const modules: Record<string, () => Promise<any>> = import.meta.glob('./pages/**/*.tsx');
console.log('modules: ', modules);
const createLazyComponent = (path: string) => {
const Component = lazy(modules[`./${path}.tsx`]);
return (
<Suspense fallback={<div>loading...</div>}>
<Component key={path} />
</Suspense>
);
};
const handleAsyncRoutes = (routes: RouteConfig[]): any =>
routes.map((route) => {
console.group('route: ', route);
if (route.children) {
route.children = handleAsyncRoutes(route.children);
}
if (route.path && route.element) {
return route;
}
if (route.componentPath && !route.element) {
return {
...route,
path: route.path,
element: createLazyComponent(route.componentPath)
};
} else return route;
});
type RouteConfig = RouteObject & {
name?: string;
componentPath?: any;
children?: RouteConfig[];
};
const routes: RouteConfig[] = [
{
path: '/',
componentPath: 'pages/AppLayout',
// element: <AppLayout />,
children: [
{ path: '', element: <span>/</span> },
{ path: 'about', element: <span>About</span> },
{ path: 'test', element: <span>Test</span> },
{
path: 'layout',
// element: <Layout />,
componentPath: 'pages/Layout',
children: [
{ path: '1', element: <span>layout1</span> },
{ path: '2', element: <span>layout2</span> },
{
path: '3',
// element: <Layout3 />,
componentPath: 'pages/Layout3',
children: [
{ path: '1', element: <span>layout/3-1</span> },
{ path: '2', element: <span>layout/3-2</span> }
]
}
]
}
]
},
{ path: '/login', element: <span>Login</span> }
];
const routesFinal = handleAsyncRoutes(routes);
const router = createBrowserRouter(routesFinal);
export default function App() {
return (
<div className='App'>
<RouterProvider router={router} />
</div>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# dynamic-import-vars
动态import和Promise的关系 (opens new window)
Imports must start with
./or../.即所有导入都必须相对于导入文件开始
导入必须以文件扩展名结尾
# React.lazy配合Suspense实现组件延迟加载
const LazyComponent = lazy(() => loadWithDelay(import('./LazyComponent'), 2000))
const Test = () => {
const [isShow, setIsShow] = useState(false)
return (
<>
<button onClick={() => setIsShow(!isShow)}>show lazy component</button>
<Suspense fallback={<div>waiting...</div>}>
{isShow && <LazyComponent />}
</Suspense>
</>
)
}
2
3
4
5
6
7
8
9
10
11
12
13
模拟组件的延迟加载
export const loadWithDelay = (promise: Promise<any>, time: number) => {
const delay = (d: number) => new Promise((resolve) => setTimeout(resolve, d))
const delayPromise = delay(time)
return Promise.all([promise, delayPromise]).then(() => promise)
}
2
3
4
5
# 使用loadable-components动态导入
https://loadable-components.com/docs/dynamic-import/
在此之前我们通过React.lazy和Suspense实现了路由模块的懒加载和代码分割
现在我了解到loadable-components这个库也可以实现以上效果并且功能更多还支持SSR
首先安装@loadable/component
pnpm add @loadable/component
使用loadable方法加载组件
const Home = loadable(() => import("../pages/Home/index"));
还可以实现 Full dynamic import,即带变量的路径导入
const AsyncPage = loadable(
(props: { page: string }) => pMinDelay(import(`../pages/${props.page}/index.tsx`), 300),
{
fallback: <div> Layout Loading...</div>,
cacheKey: (props) => props.page,
}
);
const routes: RouteObject[] = [
{
path: "layout",
element: <Layout1 />,
children: [
{
path: "1",
element: <AsyncPage page="layout1-1" />,
},
{
path: "2",
element: <AsyncPage page="layout1-2" />,
},
],
},
];
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
需要注意的是,如果你没有使用babel插件(@loadable/babel-plugin),那么你在使用Full dynamic import时,必须要设置cacheKey字段
当页面因加载过快出现闪烁问题时,我们还可以通过 p-min-delay 来延迟加载,以优化用户体验
import pMinDelay from "p-min-delay";
const Home = loadable(() => pMinDelay(import("../pages/Home/index"), 300);
2
import pMinDelay from "p-min-delay";
const AsyncPage = loadable(
(props: { page: string }) => pMinDelay(import(`../pages/${props.page}/index.tsx`), 500),
{
fallback: <div> Layout Loading...</div>,
cacheKey: (props) => props.page,
}
);
2
3
4
5
6
7
8
# pMinDelay实现
pMinDelay源码很简单:
function pMinDelay(promise, minimumDelay, options = {}) {
let delayPromise = new Promise(resolve => setTimeout(resolve, minimumDelay));
// If `delayRejection` is true, delay both fulfillment and rejection of the promise
if (options.delayRejection) {
return Promise.all([promise, delayPromise]).then(
values => values[0],
error => Promise.reject(error)
);
}
// If `delayRejection` is false or not provided, just race the two promises
return Promise.all([promise, delayPromise]).then(() => promise);
}
export default pMinDelay;
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
p-min-delay 确保:
- 至少等待设定的延迟时间。
- 如果 Promise 比设定时间早完成,延迟时间过后立即返回结果。
- 如果 Promise 加载时间超过了设定的延迟时间,Promise 一完成就会立即返回结果,不会额外等待。
来看两段代码,这两段代码有什么区别?
const loadWithDelay = (cb: Promise<any>, time: number) =>
new Promise((resolve) =>
setTimeout(() => {
resolve(cb);
}, time)
);
2
3
4
5
6
返回的 Promise 在指定的延迟后立即fullfilled,并返回传入的 Promise 对象,而不管它的状态。
const loadWithDelay = (cb: Promise<any>, time: number) =>
new Promise((resolve) =>
setTimeout(() => {
resolve();
}, time)
).then(() => cb);
2
3
4
5
6
上面的代码优化下:
const loadWithDelay = (cb: Promise<any>, time: number) =>
new Promise((resolve) => setTimeout(resolve, time)).then(() => cb);
2
返回的 Promise 在指定的延迟后执行 cb,并根据 cb 的结果来决定返回的 Promise 的解决或拒绝状态。
两段代码都可以模拟模块的延迟加载
# 手动实现pMinDelay
现在让我们使用setTimeout手动实现一个pMinDelay
const loadWithDelay = (promise: Promise<any>, time: number) => {
const delay = (d: number) => new Promise((resolve) => setTimeout(resolve, d));
const delayPromise = delay(time);
return Promise.all([promise, delayPromise]).then(() => promise);
};
2
3
4
5
使用loadWithDelay
const Test = lazy(() => loadWithDelay(import('../pages/Test/index.tsx'), 2000))